----------------------------------------
--
-- Core module (based on Faceroller.lua by hungtar)
--
local ADDON_NAME, NAMESPACE = ...

local VERSION = {string.split('.', _G['GetAddOnMetadata']('Rotor', 'Version'))}
local VERSION_NUMBER = tonumber(VERSION[1]) * 100 * 100 + tonumber(VERSION[2]) * 100 + tonumber(VERSION[3])

----------------------------------------
--
-- Stub for localization.
--
local L = setmetatable({}, {
    __index = function(t, v)
        t[v] = v
        return v
    end
})

----------------------------------------
--
-- Configuration
--
local default_config = {
    version_number = VERSION_NUMBER,
    debug = false,

    locked = false,

    time_to_wait_of_send_after_gcd = 0.5,

    framescale = 1.0,
    framealpha = 1.0,

    showinraid = true,
    showinparty = true,
    showinpvp = true,
    showwhensolo = true,
    showinvehicles = false,

    boss_only = false,

    silent = true,

    module_opts = {},
    plugin_opts = {},

    backdropcolor = {0.14, 0.14, 0.14, 0.75},
    bordercolor = {0.3, 0.3, 0.3, 0.75},
    icon_shade_color = {0.3, 0.3, 0.3},
    oor_icon_shade_color = {0.7, 0, 0},

    button_size = 40,
    icon_spacing = 1,
}
----------------------------------------
local function initDefaultConfig()
    local cfg = {}
    for k, v in pairs(default_config) do
        cfg[k] = v
    end
    _G['RotorConfigDB'] = cfg
    return cfg
end

----------------------------------------
--
-- Local references to globals for speed and lint's happiness
--
local _G = _G
local GetTime = _G['GetTime']
local GetSpellCooldown = _G['GetSpellCooldown']
local GetSpellInfo = _G['GetSpellInfo']
local UnitAura = _G['UnitAura']
local pairs = _G['pairs']
local type = _G['type']
local IsSpellInRange = _G['IsSpellInRange']
local UnitPower = _G['UnitPower']
local GetComboPoints = _G['GetComboPoints']
local UnitGUID = _G['UnitGUID']
local strsplit = _G['strsplit']
local UIParent = _G['UIParent']
local CreateFrame = _G['CreateFrame']
local UnitName = _G['UnitName']
local UnitCanAttack = _G['UnitCanAttack']
local UnitIsDead = _G['UnitIsDead']
local UnitInVehicle = _G['UnitInVehicle']
local DEFAULT_CHAT_FRAME = _G['DEFAULT_CHAT_FRAME']
local UnitClass = _G['UnitClass']
local GetActiveTalentGroup = _G['GetActiveTalentGroup']
local GetPrimaryTalentTree = _G['GetPrimaryTalentTree']
local GetSpellTabInfo = _G['GetSpellTabInfo']
local GetSpellBookItemName = _G['GetSpellBookItemName']
local BOOKTYPE_SPELL = _G['BOOKTYPE_SPELL']
local NUM_GLYPH_SLOTS = _G['NUM_GLYPH_SLOTS']
local GetGlyphSocketInfo = _G['GetGlyphSocketInfo']
local GetInventoryItemLink = _G['GetInventoryItemLink']
local GetNumRaidMembers = _G['GetNumRaidMembers']
local GetNumPartyMembers = _G['GetNumPartyMembers']
local GetInstanceInfo = _G['GetInstanceInfo']
local SlashCmdList = _G['SlashCmdList']
local format = _G['format']
local UnitLevel = _G['UnitLevel']
local UnitCastingInfo = _G['UnitCastingInfo']
local UnitIsPlayer = _G['UnitIsPlayer']
local UnitHealthMax = _G['UnitHealthMax']
local SPELL_POWER_HOLY_POWER = _G['SPELL_POWER_HOLY_POWER']
local IsSpellKnown = _G['IsSpellKnown']
local GetNumTalents = _G['GetNumTalents']
local GetTalentInfo = _G['GetTalentInfo']
local UnitExists = _G['UnitExists']
local IsUsableSpell = _G['IsUsableSpell']
local GetRuneType = _G['GetRuneType']
local GetRuneCooldown = _G['GetRuneCooldown']

----------------------------------------
--
-- The Rotor
--
local Rotor = {
    --
    -- Public fields
    --
    version = VERSION,

    visible = false,
    on_target = false,
    in_combat = false,
    power = 0,
    combo_points = 0,
    pet_exists = false,
    pet_alive = false,

    target_guid = nil,
    target_is_player = false,
    target_level = 0,
    target_max_hp = 0,

    spec = 0,
    player_level = 0,
    talents = {}, -- table of talent name -> talent rank
    talents_sig = '',
    glyphs = {}, -- table of glyph id -> true

    end_of_gcd = 0,
    spellcast_start_time = 0,
    spellcast_end_time = 0,

    default_texture = 'Interface\\Icons\\INV_Misc_PocketWatch_01',

    runes = {
        -- cooldown and types
        {cd = 0, rt = 0},   -- blood 1
        {cd = 0, rt = 0},   -- blood 2
        {cd = 0, rt = 0},   -- unholy 1
        {cd = 0, rt = 0},   -- unholy 2
        {cd = 0, rt = 0},   -- frost 1
        {cd = 0, rt = 0},   -- frost 2
        counts = {0, 0, 0, 0},
        rune_counts_sig = 0,
    },

    --
    -- Internal config
    --
    update_frequency = 0.2,
    next_skill_check_interval = 0.2,

    --
    -- Private fields
    --
    modules = {},
    plugins = nil,

    spells = {},
    icons = {},
    auras = {},
    gcd_spell = 0,
    cur_mod = nil, -- currently selected module
    player_in_raid = false,
    player_in_party = false,
    player_in_pvp = false,
    -- only enable this for classes that might need them
    power_update = false,
    combo_update = false,
    runes_update = false,
}
_G['Rotor'] = Rotor

local gcd = 0    -- gcd at update moment
local now = 0    -- time at update moment

----------------------------------------
--
-- Define caches
--
Rotor.spell_name_cache = setmetatable({}, {
    __index = function(t, k)
        local x = GetSpellInfo(k)
        t[k] = x
        return x
    end
})
----------------------------------------
Rotor.spell_icon_cache = setmetatable({}, {
    __index = function(t, k)
        local _, _, x = GetSpellInfo(k)
        if x == nil then
            x = Rotor.default_texture
        end

        t[k] = x
        return x
    end
})
----------------------------------------
Rotor.spell_id_cache = setmetatable({}, {
    __index = function(t, k)
        for i, v in pairs(Rotor.spells) do
            if v.name == k then
                t[k] = i
                return i
            end
        end

        t[k] = -1
        return -1
    end
})

----------------------------------------
--
-- Debugging
--
local log_time_base
function Rotor:dprint(...)
    if self.cfg.debug then
        if not log_time_base then
            log_time_base = GetTime()
        end
        DEFAULT_CHAT_FRAME:AddMessage(format('RDB %.3f: ', GetTime() - log_time_base)..string.join(' ', ...))
    end
end

----------------------------------------
--
-- Printing
--
function Rotor:print(x)
    DEFAULT_CHAT_FRAME:AddMessage('|cFFFF0000Rotor:|r ' .. x)
end
----------------------------------------
function Rotor:printOptInfo(x)
    if not self.cfg.silent then
        self:print(x)
    end
end

----------------------------------------
--
-- Event handler
--
local EventHandler = CreateFrame('Frame')
function EventHandler:onUpdate(elapsed)
    Rotor:update(elapsed)
end
function EventHandler:onEvent(event, ...)
    if Rotor.cfg and Rotor.cfg.debug then
        Rotor:dprint('EVENT: ', event, ...)
    end
    Rotor[event](Rotor, ...)
end
EventHandler:SetScript('OnEvent', EventHandler.onEvent)
EventHandler:SetScript('OnUpdate', EventHandler.onUpdate)
EventHandler:RegisterEvent('ADDON_LOADED')

----------------------------------------
--
-- Event registration/unregistration
--
function Rotor:registerEvent(event)
    EventHandler:RegisterEvent(event)
end

function Rotor:unregisterEvent(event)
    EventHandler:UnregisterEvent(event)
end

----------------------------------------
--
-- Utilites
--
local function timeval(t)
    if t < 0 then
        return 0
    else
        return t
    end
end
----------------------------------------
local function getTalents()
    local tab
    local sig = ''
    local ttbl = {}

    for tab = 1, 3 do
        local numTalents = GetNumTalents(tab)
        local talent = 0
        for talent = 1, numTalents do
            local talent_name, _, _, _, current_rank, _, _, _ = GetTalentInfo(tab, talent)
            ttbl[talent_name] = current_rank
            sig = sig..tostring(current_rank)
        end
    end
    return ttbl, sig
end
----------------------------------------
local function getGlyphs()
    local glyphs = {}
    local talentGroup = GetActiveTalentGroup()

    for i = 1, NUM_GLYPH_SLOTS do
        local enabled, _, _, glyph_id = GetGlyphSocketInfo(i, talentGroup)
        if enabled and glyph_id then
            glyphs[glyph_id] = true
        end
    end
    return glyphs
end
----------------------------------------
local BL_BUFF_IDS = {
    32182,  -- heroism
    2825,   -- blood lust
    80353,  -- time warp
    90355   -- ancient hysteria
}
function Rotor:initBlIndicator(icon_pos)
    for _, bl_id in pairs(BL_BUFF_IDS) do
        self:RegisterBuff(bl_id, true, false, icon_pos)
    end
end

----------------------------------------
--
-- Initialization
--
function Rotor:initialize()
    self.cfg = _G['RotorConfigDB'] or initDefaultConfig()

    -- check add new config params
    for k, v in pairs(default_config) do
        if self.cfg[k] == nil then
            self.cfg[k] = v
        end
    end

    if (self.cfg.version_number or 0) < VERSION_NUMBER then
        self:updateConfigToCurrentVersion()
    end

    -- check remove obsolete params
    local keys_to_remove = {}
    for k in pairs(self.cfg) do
        if default_config[k] == nil then
            keys_to_remove[k] = true
        end
    end
    for k in pairs(keys_to_remove) do
        self.cfg[k] = nil
    end

    self:initSlashCommands()

    local _, playerClass = UnitClass('player')

    self.player_class = playerClass

    if self.player_class == 'ROGUE' or self.player_class == 'DRUID' or self.player_class == 'PALADIN' then
        self.combo_update = true
        if self.player_class == 'PALADIN' then
            self.get_combo_points_func = function () return UnitPower('player', SPELL_POWER_HOLY_POWER) end
        else
            self.get_combo_points_func = function() return GetComboPoints('player', 'target') end
        end
    else
        self.combo_update = false
    end

    self.runes_update = self.player_class == 'DEATHKNIGHT'

    self.power_update = true

    self:createFrames('RotorDisplay')

    self:showOrHideAnchor()

    if self.plugins then
        for _, plugin in pairs(self.plugins) do
            plugin:Init(self.plugin_opts[plugin:Name()] or {})
        end
    end

    self:registerEvent('SKILL_LINES_CHANGED')
end
----------------------------------------
function Rotor:updateConfigToCurrentVersion()
    --
    -- do update here based on self.cfg.version_number
    --
    self.cfg.version_number = VERSION_NUMBER
end

----------------------------------------
--
-- Slash command functions
--
function Rotor:initSlashCommands()

    local slashCommands = {
        debug = Rotor.toggleDebug,
        version = Rotor.printVersion,
        lock = Rotor.toggleLock,
        scale = Rotor.setScaleOpt,
        alpha = Rotor.setAlphaOpt,
        list = Rotor.listModules,
        opt = Rotor.moduleOption,
        reset = Rotor.resetOptions,
        show = Rotor.setShowCondition,
        silent = Rotor.toggleSilent,
    }

    SlashCmdList['ROTOR'] = function(cmd)
        local cmd, args = strsplit(' ', cmd:lower(), 2)
        if slashCommands[cmd] then
            return slashCommands[cmd](self, args)
        else
            self:printHelp()
        end
    end

    _G['SLASH_ROTOR1'] = '/rotor'
    _G['SLASH_ROTOR2'] = '/rr'
end
----------------------------------------
function Rotor:printHelp()
    self:print(L['Rotor options:'])
    self:print(L['version - print version number.'])
    self:print(L['lock - lock/unlock the frame.'])
    self:print(L['scale <num> - set scaling of the frame.'])
    self:print(L['alpha <num> - set alpha of the frame.'])
    self:print(L['list - print a list of available modules.'])
    self:print(L['opt <X> - pass X to the option handling function of the active module.'])
    self:print(L['show <X> - configure when to show Rotor, /rr show for help.'])
    self:print(L['range - toggle range check.'])
    self:print(L['power - toggle power check.'])
    self:print(L['reset [pos] - reset all options. if pos then reset also position'])
    self:print(L['silent - suppress some chat output.'])
    self:print(L['bossonly - toggle show only on boss targets.'])
end
----------------------------------------
function Rotor:printVersion()
    self:print(VERSION[1] .. '.' .. VERSION[2] .. '.' .. VERSION[3])
end
----------------------------------------
function Rotor:toggleDebug()
    if self.cfg.debug then
        self.cfg.debug = false
        self:print(L['debug disabled.'])
    else
        self.cfg.debug = true
        self:print(L['debug enabled.'])
    end
end
----------------------------------------
function Rotor:toggleLock()
    self.cfg.locked = not self.cfg.locked
    self:showOrHideAnchor()
    if not self.cfg.locked then
        self:print(L['frame unlocked.'])
    else
        self.cfg.locked = true
        self:print(L['frame locked.'])
    end
end
----------------------------------------
function Rotor:setScaleOpt(x)
    if x == nil then
        self:print(L['argument required.'])
        return
    end

    self.cfg.framescale = tonumber(x)
    self.anchor:SetScale(self.cfg.framescale)
    self.main_icon:SetScale(self.cfg.framescale)
    for _, icon in pairs(self.icons) do
        icon:SetScale(self.cfg.framescale)
    end
    self:pluginsDo(function(plugin) plugin:ScaleChanged(self.cfg.framescale) end)
end
----------------------------------------
function Rotor:listModules()
    self:print(L['available modules:'])
    for n, v in pairs(self.modules) do
        self:print(n)
    end
end
----------------------------------------
function Rotor:setModuleOption(x)
    if self.cur_mod:HandleCommandLine(x) then
        self:chooseModule()
    end
end
----------------------------------------
function Rotor:setAlphaOpt(x)
    if x == nil then
        self:print(L['argument required.'])
        return
    end

    self.cfg.framealpha = tonumber(x)
    self.main_icon:SetAlpha(self.cfg.framealpha)
    for _, icon in pairs(self.icons) do
        icon:SetAlpha(self.cfg.framealpha)
    end
    self:pluginsDo(function(plugin) plugin:AlphaChanged(self.cfg.framealpha) end)
end
----------------------------------------
function Rotor:resetOptions(x)
    local old_locked = self.cfg.locked

    initDefaultConfig()

    self.cfg = _G['RotorConfigDB']

    if x ~= 'pos' then
        self.cfg.locked = old_locked
    end

    if x == 'pos' then
        self.cfg.locked = false
        self.anchor:SetPoint('CENTER', UIParent, 'CENTER', 0, 0)
        self.main_icon:SetScale(self.cfg.framescale)
        self.main_icon:SetAlpha(self.cfg.framealpha)

        self:showOrHideAnchor()
    end

    self:print(L['options have been reset.'])
end
----------------------------------------
local function toggle(opt_name, opt_descr)
    Rotor.cfg[opt_name] = not Rotor.cfg[opt_name]
    if Rotor.cfg[opt_name] then
        Rotor:print(L['Rotor will show ']..opt_descr..'.')
    end
end
----------------------------------------
local function printShowOpt(opt, descr)
    if opt then
        Rotor:print(' '..descr)
    end
end
----------------------------------------
function Rotor:setShowCondition(arg)
    if arg == 'raid' then
        toggle('showinraid', L['in raids'])
    elseif arg == 'party' then
        toggle('showinparty', L['in parties'])
    elseif arg == 'solo' then
        toggle('showwhensolo', L['when solo'])
    elseif arg == 'pvp' then
        toggle('showinpvp', L['in pvp'])
    elseif arg == 'vehicles' then
        toggle('showinvehicles', L['in vehicles'])
    elseif arg == 'bossonly' then
        toggle('boss_only', L['on boss targets only'])
    else
        self:print(L['Show Options:'])
        self:print(L[' show raid - hide/show in raids.'])
        self:print(L[' show party - hide/show in parties.'])
        self:print(L[' show solo - hide/show solo.'])
        self:print(L[' show pvp - hide/show in pvp.'])
        self:print(L[' show vehicles - hide/show in vehicles.'])
        self:print(L[' show bossonly - show on boss targets only.'])
        self:print(L['Rotor will currently show:'])
        printShowOpt(self.cfg.showinraid, L['in raids'])
        printShowOpt(self.cfg.showinparty, L['in party'])
        printShowOpt(self.cfg.showwhensolo, L['when solo'])
        printShowOpt(self.cfg.showinpvp, L['in pvp'])
        printShowOpt(self.cfg.showinvehicles, L['in vehicles'])
        printShowOpt(self.cfg.boss_only, L['only on bosses'])
    end
    self:checkTarget()
end
----------------------------------------
function Rotor:toggleBoolOpt(opt_name, descr)
    self.cfg[opt_name] = not self.cfg[opt_name]
    if self.cfg[opt_name] then
        self:print(descr..' '..L['enabled']..'.')
    end
end
----------------------------------------
function Rotor:toggleSilent(x)
    self:toggleBoolOpt('silent', L['silent mode'])
end

----------------------------------------
--
-- Update function
--
local last_spell = 0
local last_gcd = 0
local wait_for_send_till = 0
local last_update = 0
local last_cnsf = 0
local last_aura_scan_time = 0
----------------------------------------
function Rotor:forceUpdate(delay)
    last_cnsf = 0
    if delay == 0 then
        -- update immediately
        last_update = 0
        self:update(0)
    else
        last_update = GetTime() + (delay or 0.01) - Rotor.update_frequency
    end
end
----------------------------------------
function Rotor:defaultRangeCheck(spell_id)
    return IsSpellInRange(self.spell_name_cache[spell_id]) ~= 0
end
----------------------------------------
function Rotor:defaultUsableCheck(spell_id)
    return IsUsableSpell(Rotor.spell_name_cache[spell_id]) == 1
end
----------------------------------------
function Rotor:checkCurSpellRangeAndUsability()
    local spell_usable, spell_in_range = true, true
    if last_spell ~= 0 then
        spell_usable = self.cur_mod:CheckUsable(last_spell)
        spell_in_range = self.cur_mod:CheckInRange(last_spell)
    end

    if not spell_in_range then
        self.main_icon:shade(self.cfg.oor_icon_shade_color)
    elseif not spell_usable then
        self.main_icon:shade()
    else
        self.main_icon:unshade()
    end
end
----------------------------------------
function Rotor:updateSpells()
    local old_ends, was_usable

    for spell_id, spell in pairs(self.spells) do
        spell.cooldown_start, spell.cooldown_duration = GetSpellCooldown(spell_id)
        if spell.cooldown_start == 0 then
            spell.cooldown_duration = 0
        end
        old_ends = spell.ends
        spell.ends = spell.cooldown_start + spell.cooldown_duration
        spell.cd = timeval(spell.ends - now)
        was_usable = spell.usable
        spell.usable = self.cur_mod:CheckUsable(spell.id) or false
        if spell.icon and ((old_ends ~= spell.ends) or (was_usable ~= spell.usable)) then
            spell.icon:update()
        end
    end
end
----------------------------------------
function Rotor:updateGlobalCooldown()
    local start, duration = GetSpellCooldown(self.gcd_spell)
    if start and start > 0 then
        self.end_of_gcd = start + duration
    else
        self.end_of_gcd = 0
        start = 0
        duration = 0
    end
    self.main_icon:setCooldown(start, duration)
end
----------------------------------------
function Rotor:updateRunes()
    self.runes.counts[1] = 0
    self.runes.counts[2] = 0
    self.runes.counts[3] = 0
    self.runes.counts[4] = 0
    for i = 1, 6 do
        local rt = GetRuneType(i)
        self.runes[i].rt = rt
        local s, d = GetRuneCooldown(i)
        if s == 0 then
            self.runes[i].cd = 0
        elseif s > now then
            self.runes[i].cd = d + (now - s)
        else
            self.runes[i].cd = s + d - now
            if self.runes[i].cd < 0.001 then
                self.runes[i].cd = 0
            end
        end
        if self.runes[i].cd == 0 then
            self.runes.counts[rt] = self.runes.counts[rt] + 1
        end
    end
    self.runes.rune_counts_sig =
        self.runes.counts[1] +
        self.runes.counts[2] * 10 +
        self.runes.counts[3] * 100 +
        self.runes.counts[4] * 1000
end
----------------------------------------
function Rotor:updateResourcesAndCooldowns()
    if self.power_update then
        local last_power = self.power
        self.power = UnitPower('player')
        if self.power_display_visible and (last_power ~= self.power) then
            self:updatePowerDisplay()
        end
    end

    if self.combo_update then
        self.combo_points = self.get_combo_points_func()
        if self.upper_text_visible then
            self.upper_text:SetText(self.combo_points)
        end
    end

    if self.runes_update then
        self:updateRunes()
    end

    self:checkPet()

    self:beginDeferIconUpdate()
    self:updateGlobalCooldown()
    self:updateAuras()
    self:updateSpells()
    self:endDeferIconUpdate()

    if self.plugins then
        for _, plugin in pairs(self.plugins) do
            plugin:ResourcesAndCooldownsUpdated()
        end
    end
end
----------------------------------------
function Rotor:update(elapsed)
    now = GetTime()
    gcd = timeval(self.end_of_gcd - now)

    if self.module_switch_time then
        if self.module_switch_time <= now then
            self.module_switch_time = nil
            self:chooseModule()
        end
        return
    end

    if self.plugins then
        for _, plugin in pairs(self.plugins) do
            plugin:update(elapsed)
        end
    end

    if not self.visible then
        return
    end

    if self.spellcast_end_time ~= 0 then
        if self.spellcast_end_time < now then
            self.spellcast_start_time = 0
            self.spellcast_end_time = 0
        end
    end

    if self.cur_mod then
        self.cur_mod:Update(elapsed)
    end

    if (now - last_update) < self.update_frequency then
        return
    end
    last_update = now

    self:updateResourcesAndCooldowns()

    if self.on_target then
        if (now - last_cnsf) > self.next_skill_check_interval then
            last_cnsf = now
            self:updateNextSkill()
        end

        self:checkCurSpellRangeAndUsability()
    end
end

----------------------------------------
--
-- call the active module nextskill function
--
function Rotor:updateNextSkill()
    local spell

    if gcd > last_gcd then
        -- some spell sent
        spell = 0 -- new spell required
        wait_for_send_till = now + gcd + (self.cur_mod:TimeToWaitOfSend() or self.cfg.time_to_wait_of_send_after_gcd)
    elseif now > wait_for_send_till then
        spell = 0 -- send wait complete, new spell required
    else
        spell = last_spell
    end
    last_gcd = gcd

    self.target_max_hp = UnitHealthMax('target')

    spell = self.cur_mod:NextSkill(gcd, spell)

    if spell ~= last_spell then
        last_spell = spell
        self.main_icon:setTexture(self.spell_icon_cache[spell])
        if spell == 0 then
            self.main_icon:setTexture(nil)
        else
            self.main_icon:setTexture(self.spell_icon_cache[spell])
        end
    end
end

----------------------------------------
--
-- User interface functions
--
function Rotor:showOrHideAnchor()
    if self.cfg.locked then
        self.anchor:EnableMouse(false)
        self.anchor:Hide()
    else
        self.anchor:EnableMouse(true)
        self.anchor:Show()
    end
end
----------------------------------------
function Rotor:createFrames(name)
    self:initIcons()
    -- anchor for spell frame
    local anchor = CreateFrame('Frame', name .. '_Anchor', UIParent)
    anchor:RegisterForDrag('LeftButton')
    local anchor_size = self.button_size + self.icon_size * 2 + self.cfg.icon_spacing * 2 + 4
    anchor:SetWidth(anchor_size)
    anchor:SetHeight(anchor_size)
    anchor:SetMovable(true)
    anchor:EnableMouse(true)
    anchor:ClearAllPoints()
    anchor:SetPoint('CENTER', UIParent, 'CENTER', 0, 0)
    anchor.bg = anchor:CreateTexture(name .. '_AnchorBG', 'PARENT')
    anchor.bg:SetAllPoints(anchor)
    anchor.bg:SetBlendMode('BLEND')
    anchor.bg:SetTexture(0, 0, 0, 0.4)
    anchor:SetScript('OnDragStart', EventHandler.StartMoving)
    anchor:SetScript('OnDragStop', EventHandler.StopMovingOrSizing)
    anchor:SetScale(self.cfg.framescale)
    self.anchor = anchor

    -- spell frame
    self.main_icon = self:createIcon(name .. '_Button')
    self.main_icon:SetPoint('CENTER', anchor, 'CENTER')

    local power_text = self.main_icon:CreateFontString(name..'_Power', 'OVERLAY')
    power_text:SetFont('Fonts\\ArialN.ttf', 14, 'OUTLINE')
    power_text:SetShadowColor(0, 0, 0)
    power_text:SetShadowOffset(1, -1)
    power_text:SetWidth(self.button_size - 6)
    power_text:SetHeight(math.floor(self.button_size / 3))
    power_text:SetJustifyH('CENTER')
    power_text:SetJustifyV('BOTTOM')
    power_text:ClearAllPoints()
    power_text:SetPoint('BOTTOMLEFT', self.main_icon, 'BOTTOMLEFT', 3, 1)
    power_text:SetText('0')
    power_text:Hide()
    self.power_text = power_text
    self.power_display_visible = false

    local upper_text = self.main_icon:CreateFontString(name..'_UpperText', 'OVERLAY')
    upper_text:SetFont('Fonts\\ArialN.ttf', 14, 'OUTLINE')
    upper_text:SetShadowColor(0, 0, 0)
    upper_text:SetShadowOffset(1, -1)
    upper_text:SetWidth(self.button_size - 6)
    upper_text:SetHeight(math.floor(self.button_size / 3))
    upper_text:SetJustifyH('CENTER')
    upper_text:SetJustifyV('TOP')
    upper_text:ClearAllPoints()
    upper_text:SetPoint('TOPLEFT', self.main_icon, 'TOPLEFT', 3, -3)
    upper_text:SetText('')
    upper_text:Hide()
    self.upper_text = upper_text
    self.upper_text_visible = false
end
----------------------------------------
function Rotor:showPowerDisplay(initial_text)
    if not self.power_display_visible then
        self.power_text:SetText(initial_text or '0')
        self.power_text:Show()
        self.power_display_visible = true
    end
end
----------------------------------------
function Rotor:hidePowerDisplay()
    if self.power_display_visible then
        self.power_text:Hide()
        self.power_display_visible = false
    end
end
----------------------------------------
function Rotor:updatePowerDisplay()
    if self.power > 1000 then
        self:setPowerText(math.floor((self.power + 500) / 1000) + 'k')
    else
        self:setPowerText(tostring(self.power))
    end
    self.cur_mod:PowerDisplayUpdated()
end
----------------------------------------
local last_power_text = ''
function Rotor:setPowerText(text)
    if last_power_text ~= text then
        self.power_text:SetText(text)
        last_power_text = text
    end
end
----------------------------------------
function Rotor:showUpperText(initial_text)
    if not self.upper_text_visible then
        self.upper_text_visible = true
        self:setUpperText(initial_text or '')
        self.upper_text:Show()
    end
end
----------------------------------------
function Rotor:hideUpperText()
    if self.upper_text_visible then
        self.upper_text_visible = false
        self.upper_text:Hide()
    end
end
----------------------------------------
local last_upper_text = ''
function Rotor:setUpperText(text)
    if text ~= last_upper_text then
        self.upper_text:SetText(text)
        last_upper_text = text
    end
end
----------------------------------------
function Rotor:hide()
    if not self.visible then
        return
    end
    for _, icon in pairs(self.icons) do
        icon:hide()
    end
    self.main_icon:hide()
    self.visible = false
    self:pluginsDo(function(plugin) plugin:MainInterfaceVisibilityChanged(self.visible) end)
end
----------------------------------------
function Rotor:show()
    if self.visible then
        return
    end
    self.main_icon:show()
    self.visible = true
    self:pluginsDo(function(plugin) plugin:MainInterfaceVisibilityChanged(self.visible) end)
end

----------------------------------------
--
-- API functions
--

-- HasGlyph(spellid):
--
-- Check if player has the glyph spellid. Returns true if glyph is found,
-- false otherwise.
--
function Rotor:HasGlyph(id)
    return self.glyphs[id]
end

--
-- GetItemId(slot):
--
function Rotor:GetItemId(x)
    local l = GetInventoryItemLink('player', x)

    if l == nil then
        return 0
    end

    local _, ret = string.split(':', l)
    return tonumber(ret)
end

--
-- GetSetItemSlotItems():
--
-- Returns the item ids of head, shoulder, chest, legs and gloves
-- item slot(in that order).
--
function Rotor:GetSetItemSlotItems()
    -- head, shoulder, chest, legs, gloves
    return self:GetItemId(1), self:GetItemId(3), self:GetItemId(5), self:GetItemId(7), self:GetItemId(10)
end

--
-- HasSpell(spell):
--
-- find spell in players spellbook. Return true when found,
-- false otherwise.
--
function Rotor:HasSpell(spell, fail_silently)
    if not spell then
        self:print('INVALID SPELL (nil)')
        return false
    end

    local result = false

    if type(spell) == 'number' then
        result = IsSpellKnown(spell)
    else
        for t = 1, 4 do
            local _, _, o, n = GetSpellTabInfo(t)

            for i = (1 + o), (o + n) do
                local s = GetSpellBookItemName(i, BOOKTYPE_SPELL)

                if (s == spell) then
                    result = true
                    break
                end
            end
        end
    end

    if not result and not fail_silently then
        self:print('HasSpell for "' .. tostring(spell) .. '" failed.')
    end

    return result
end

--
-- Auras
--
local Aura = {
    count = 0,
    time_left = 0,
    active = false,
    ends = 0,
    name = '',
    unit = 'player',
    filter = 'HELPFUL',
    texture_name = Rotor.default_texture,
}
----------------------------------------
function Aura:new(spell, unit, isDebuff, fromOthers)
    local aura = {}

    for k, v in pairs(Aura) do
        if k[1] ~= '_' then
            aura[k] = v
        end
    end

    aura.name = spell
    aura.unit = unit
    aura.texture_name = Rotor.spell_icon_cache[spell]

    if isDebuff then
        aura.filter = 'HARMFUL'
    else
        aura.filter = 'HELPFUL'
    end

    if not fromOthers then
        aura.filter = aura.filter .. '|PLAYER'
    end

    return aura
end
----------------------------------------
function Aura:isPlayerAura()
    return self.unit == 'player'
end
----------------------------------------
function Aura:isPetAura()
    return self.unit == 'pet'
end
----------------------------------------
function Aura:isPlayerOrPetAura()
    return self:isPlayerAura() or self:isPetAura()
end
----------------------------------------
function Aura:isTargetAura()
    return self.unit == 'target'
end
----------------------------------------
function Aura:updateIcons()
    if self.icons then
        for _, icon in pairs(self.icons) do
            icon:update()
        end
    end
end
----------------------------------------
function Aura:update()
    if self.active and (self.duration > 0) and (self.ends < now) then
        self.active = false
        self.time_left = 0
        self.start = 0
        self.ends = 0
        self.duration = 0
        self.count = 0
        self.changed = true
    end

    if self.changed then
        self.changed = false
        self:updateIcons()
    end
end
----------------------------------------
function Aura:scan()
    local n, _, tex, cn, _, d, e = UnitAura(self.unit, self.name, nil, self.filter)
    local ctime = GetTime()
    local changed = false

    if not n or ((e <= ctime) and (d ~= 0)) then
        if self.active then
            self.active = false
            self.ends = 0
            self.count = 0
            self.time_left = 0
            changed = true
        end
    else
        changed = not self.active
            or (self.duration ~= d)
            or (self.count ~= cn)
            or (self.texture_name ~= tex)
        self.active = true
        self.duration = d
        self.texture_name = tex
        self.count = cn
        if d == 0 then
            -- endless aura
            changed = (self.time_left ~= 0) or (self.ends ~= 0) or changed
            self.time_left = 0
            self.ends = 0
        else
            self.chaned = (self.ends ~= e) or (self.time_left ~= (e - ctime)) or changed
            self.ends = e
            self.time_left = e - ctime
        end
    end

    self.changed = self.changed or changed
    return changed
end
----------------------------------------
function Aura:addIcon(icon)
    if not self.icons then
        self.icons = {}
    else
        for _, my_icon in pairs(self.icons) do
            if my_icon == icon then
                return -- already here
            end
        end
    end
    table.insert(self.icons, icon)
end
----------------------------------------
function Rotor:scanAuras(unit)
    local changed = false

    for _, aura in pairs(self.auras) do
        if not unit or aura.unit == unit then
            changed = aura:scan() or changed
        end
    end

    last_aura_scan_time = GetTime()

    return changed
end
----------------------------------------
function Rotor:updateAuras()
    for _, aura in pairs(self.auras) do
        aura:update()
    end
end
----------------------------------------
function Rotor:addAuraIcon(icon_pos, aura, behavior)
    local icon = self:getIconAtPos(icon_pos)
    icon:addAura(aura, behavior)
    aura:addIcon(icon)
end
----------------------------------------
function Rotor:registerAura(unit, spell, others, isdebuff, icon_pos, icon_behavior)
    if type(spell) == 'number' then
        spell = self.spell_name_cache[spell]
    end

    local aura = Aura:new(spell, unit, isdebuff, others)

    table.insert(self.auras, aura)

    if icon_pos then
        self:addAuraIcon(icon_pos, aura, icon_behavior)
    end

    return aura
end

--
-- RegisterBuff(spell):
--
-- Add a buff to the buffs table.
-- returns aura record
function Rotor:RegisterBuff(spell, others, isdebuff, icon_pos, icon_behavior)
    return self:registerAura('player', spell, others, isdebuff, icon_pos, icon_behavior)
end

function Rotor:RegisterSelfBuff(spell, icon_pos, icon_behavior)
    return self:RegisterBuff(spell, false, false, icon_pos, icon_behavior)
end

--
-- RegisterPetBuff
--
function Rotor:RegisterPetBuff(spell, others, icon_pos, icon_behavior)
    return self:registerAura('pet', spell, others, false, icon_pos, icon_behavior)
end

--
-- RegisterDebuff(spell [, mine]):
--
-- Add a debuff to the debuffs table. If mine is true,
-- only check for debuff cast by us.
--
-- returns debuff rec
function Rotor:RegisterDebuff(spell, mine, icon_pos, icon_behavior)
    return self:registerAura('target', spell, not mine, true, icon_pos, icon_behavior)
end

--
-- RegisterPetDebuff
--
function Rotor:RegisterPetDebuff(spell, icon_pos, icon_behavior)
    return self:registerAura('pet', spell, true, true, icon_pos, icon_behavior)
end

----------------------------------------
--
-- Spell functions
--

----------------------------------------
--
-- Spell class
--
local Spell = {
    name = '',
    id = 0,
    texture_name = '',
    cd = 600,
    usable = false,
    cooldown_start = 0,
    cooldown_duration = 0,
    ends = 0,
    sentAt = 0, -- time of UNIT_SPELLCAST_SENT
    doneAt = 0, -- time of UNIT_SPELLCAST_SUCCEEDED
    aura_rec = nil,
    is_pet_spell = false,
    off_gcd = false,
    def = nil,
}
----------------------------------------
local SpellDef = {
    id = 0,
    icon_pos = 0,
    icon_behavior = 'default',
    off_gcd = false,
    with_buff = false,
    with_debuff = false,
    must_exists = false,
    is_pet_spell = false,
    range_check = nil,
    min_power = nil,
    min_combo_points = nil,
}
----------------------------------------
function Spell:new(spell_id, extra)
    local spell = {}

    -- clone Spell prototype
    for k, v in pairs(Spell) do
        spell[k] = v
    end

    spell.name = Rotor.spell_name_cache[spell_id]
    spell.id = spell_id
    spell.texture_name = Rotor.spell_icon_cache[spell_id]

    if extra then
        for k, v in pairs(extra) do
            spell[k] = v
        end
    end

    return spell
end
----------------------------------------
function Spell:isUnsent(time_since_last_send)
    return (self.sentAt <= self.doneAt) and (self.doneAt + (time_since_last_send or 0.5) < now)
end
----------------------------------------
function Spell:isOffCdAndUnsent(time_since_last_send, wth)
    return ((self.cd - (wth or 0.4)) <= gcd) and self:isUnsent(time_since_last_send)
end
----------------------------------------
-- RegisterSpell
-- registers spell within Rotor for tracking colldowns, usablity etc
-- returns spell struct
function Rotor:RegisterSpell(spell_id, icon_pos, icon_behavior, spell_aura, extra)
    local spell = Spell:new(spell_id, extra)

    spell.aura = spell_aura
    self.spells[spell_id] = spell

    if icon_pos then
        spell.icon = self:getIconAtPos(icon_pos)
        spell.icon:initForSpell(spell, icon_behavior)
        if spell_aura then
            spell_aura:addIcon(spell.icon)
        end
    end

    return spell, spell.aura
end
----------------------------------------
function Rotor:RegisterOffGcdSpell(spell_id, icon_pos, icon_behavior, spell_aura)
    return self:RegisterSpell(spell_id, icon_pos, icon_behavior, spell_aura, {off_gcd = true})
end
----------------------------------------
function Rotor:RegisterOffGcdSpellWithSelfBuff(spell_id, icon_pos, icon_behavior)
    return self:RegisterOffGcdSpell(spell_id, icon_pos, icon_behavior, Rotor:RegisterSelfBuff(spell_id))
end
----------------------------------------
function Rotor:RegisterSpellWithSelfBuff(spell_id, icon_pos, icon_behavior)
    return self:RegisterSpell(spell_id, icon_pos, icon_behavior, Rotor:RegisterSelfBuff(spell_id))
end
----------------------------------------
function Rotor:RegisterSpells(spell_defs, spell_table)
    -- see SpellDef class for description of spell_defs elements
    spell_table = spell_table or {}
    if spell_defs then
        for spell_mnem, spell_def in pairs(spell_defs) do
            local aura
            if not spell_def.must_exists or self:HasSpell(spell_def.id, true) then
                if spell_def.with_buff then
                    aura = self:RegisterSelfBuff(spell_def.id)
                elseif spell_def.with_debuff then
                    aura = self:RegisterDebuff(spell_def.id, true)
                end
                local spell = self:RegisterSpell(spell_def.id, spell_def.icon_pos, spell_def.icon_behavior, aura)
                spell.def = spell_def
                spell.off_gcd = spell_def.off_gcd
                spell.is_pet_spell = spell_def.is_pet_spell
                spell.min_power = spell_def.min_power
                spell.range_check = spell_def.range_check
                if type(spell_mnem) == 'string' then
                    spell_table[spell_mnem] = spell
                end
            end
        end
    end

    return spell_table
end
----------------------------------------
function Rotor:RegisterAuras(aura_defs, buff_table, debuff_table)
--[[
    aura_def:
        aura_mnemonic={id=AURA_ID, is_debuff=IS_DEBUFF, from_others=FROM_OTHERS, is_pet_aura=IS_PET_AURA, icon_pos=ICON_POS, icon_behavior=ICON_BEHAVIOR}
    returns table of buffs and table of debuffs indexed by mnemonics
--]]
    buff_table = buff_table or {}
    debuff_table = debuff_table or {}
    if aura_defs then
        for aura_mnem, aura_def in pairs(aura_defs) do
            local unit

            unit = aura_def.unit
            if not unit then
                if aura_def.is_pet_buff then
                    unit = 'pet'
                elseif aura_def.is_debuff then
                    unit = 'target'
                else
                    unit = 'player'
                end
            end
            local aura = self:registerAura(unit, aura_def.id, aura_def.from_others, aura_def.is_debuff, aura_def.icon_pos, aura_def.icon_behavior)
            if type(aura_mnem) == 'string' then
                if aura_def.is_debuff then
                    debuff_table[aura_mnem] = aura
                else
                    buff_table[aura_mnem] = aura
                end
            end
        end
    end

    return buff_table, debuff_table
end
----------------------------------------
function Rotor:RegisterSpellsAndAuras(spell_defs, aura_defs, spell_table, buff_table, debuff_table)
    spell_table = self:RegisterSpells(spell_defs, spell_table)
    buff_table, debuff_table = self:RegisterAuras(aura_defs, buff_table, debuff_table)

    return spell_table, buff_table, debuff_table
end
----------------------------------------
function Rotor:pluginsDo(func, ...)
    if self.plugins then
        for _, plugin in pairs(self.plugins) do
            func(plugin, ...)
        end
    end
end

----------------------------------------
--
-- Current module finalization
--
function Rotor:finalizeCurrentModule()
    if not self.cur_mod then
        -- already finalized
        return
    end

    local old_mod = self.cur_mod

    self:unregisterEvents()

    self:hidePowerDisplay()

    self:hideUpperText()

    self:hide()

    self.cur_mod = nil

    self.target_guid = nil
    self.target_is_player = false

    if old_mod then
        old_mod:Done()
    end

    self:initIcons()

    self.auras = {}
    self.spells = {}

    self:pluginsDo(function(plugin) plugin:CurrentModuleFinalized() end)
end

--
-- chooseModule():
--
-- select module for the current spec.
--
local function isSameGlyphs(g1, g2)
    for id, _ in pairs(g1) do
        if not g2[id] then
            return false
        end
    end
    for id, _ in pairs(g2) do
        if not g1[id] then
            return false
        end
    end
    return true
end
----------------------------------------
function Rotor:chooseModule()
    local tal_tbl, tal_sig = getTalents()
    local glyphs = getGlyphs()
    local old_mod = self.cur_mod

    if (self.spec == (GetPrimaryTalentTree() or 0))
        and (self.talent_sig ~= tal_sig)
        and isSameGlyphs(glyphs, self.glyphs)
    then
        return
    end

    self.spec = GetPrimaryTalentTree() or 0
    self.player_level = UnitLevel('player')
    self.talents = tal_tbl
    self.talents_sig = tal_sig
    self.glyphs = glyphs

    local new_mod = nil
    for _, rmod in pairs(self.modules) do
        if rmod:Detect() then
            new_mod = rmod
            break
        end
    end

    if new_mod and (new_mod == self.cur_mod) then
        return
    end

    self:finalizeCurrentModule()

    if not new_mod then
        self:print(L['No module found for current spec/class.'])
        return
    end

    local s
    local m
    local opts = nil

    if not self.cfg.module_opts then
        self.cfg.module_opts = {}
    end
    opts = self.cfg.module_opts[new_mod:Name()] or {}

    s, m = new_mod:Init(opts)

    if not s then
        self:print(string.format(L['Error: init failed for module \':%s\' (Reason: %s)'], new_mod:Name(), m or 'none given'))
        return
    end

    self.upper_text:SetText('')

    self.cur_mod = new_mod
    self:printOptInfo(string.format(L['Using module %s.'], self.cur_mod:Name()))

    self:registerEvents()

    self:pluginsDo(function(plugin) plugin:NewModuleSelected() end)

    self:checkTarget()
end

----------------------------------------
--
-- Events
--
local Events = {
    'PLAYER_TARGET_CHANGED',
    'PLAYER_ENTERING_WORLD',
    'PLAYER_REGEN_ENABLED',
    'PLAYER_REGEN_DISABLED',
    'UNIT_INVENTORY_CHANGED',
    'PARTY_MEMBERS_CHANGED',
    'UNIT_FACTION'
}
----------------------------------------
local EventsWithValidTarget = {
    'UNIT_SPELLCAST_SENT',
    'UNIT_SPELLCAST_SUCCEEDED',
    'UNIT_SPELLCAST_FAILED',
    'UNIT_SPELLCAST_FAILED_QUIET',
    'UNIT_SPELLCAST_INTERRUPTED',
    'UNIT_SPELLCAST_DELAYED',
    'UNIT_SPELLCAST_START',
    'UNIT_SPELLCAST_STOP',
    'UNIT_PET',
    'UNIT_AURA',
    'SPELL_UPDATE_COOLDOWN',
    'ACTIONBAR_UPDATE_COOLDOWN',
    'SPELL_UPDATE_USABLE',
}
----------------------------------------
function Rotor:registerEvents()
    for _, event in pairs(Events) do
        self:registerEvent(event)
    end
end
----------------------------------------
function Rotor:unregisterEvents()
    for _, event in pairs(Events) do
        self:unregisterEvent(event)
    end
    self:unregisterEventsWithValidTarget()
end
----------------------------------------
function Rotor:registerEventsWithValidTarget()
    for _, event in pairs(EventsWithValidTarget) do
        self:registerEvent(event)
    end
    if self.cur_mod.HandleUnfilteredCombatEvent then
        self:registerEvent('COMBAT_LOG_EVENT_UNFILTERED')
    end
end
----------------------------------------
function Rotor:unregisterEventsWithValidTarget()
    for _, event in pairs(EventsWithValidTarget) do
        self:unregisterEvent(event)
    end
    self:unregisterEvent('COMBAT_LOG_EVENT_UNFILTERED')
end
----------------------------------------
function Rotor:targetAcquired(guid)
    local old_target = self.target_guid
    self.target_guid = guid
    self.target_is_player = UnitIsPlayer('target')
    self.target_level = UnitLevel('target')
    self.on_target = true

    if not old_target then
        last_spell = 0
        wait_for_send_till = 0

        self:registerEventsWithValidTarget()
    end

    self.cur_mod:TargetChanged(old_target)

    self:scanAuras()
    self:forceUpdate(0)
    self:show()
    for _, icon in pairs(self.icons) do
        icon:update()
    end
    self:pluginsDo(function(plugin) plugin:TargetChanged(old_target) end)
end
----------------------------------------
function Rotor:targetLost()
    if self.target_guid then
        local old_target = self.target_guid
        self.target_guid = nil
        self.on_target = false
        self:unregisterEventsWithValidTarget()
        self:hide()
        self.cur_mod:TargetChanged(old_target)
        self:pluginsDo(function(plugin) plugin:TargetChanged(old_target) end)
    end
end
----------------------------------------
function Rotor:checkTarget()
    local n = UnitName('target')
    local guid = UnitGUID('target')

    if (n == nil)
        or (not UnitCanAttack('player', 'target'))
        or UnitIsDead('target')
        or (self.player_in_pvp and not self.cfg.showinpvp)
        or (self.player_in_raid and not self.cfg.showinraid)
        or (not self.player_in_raid and self.player_in_party and not self.cfg.showinparty)
        or (not self.player_in_raid and not self.player_in_party and not self.cfg.showwhensolo)
        or ((not self.cfg.showinvehicles) and UnitInVehicle('player') == 1)
        or (self.boss_only and (UnitLevel('target') ~= -1))
    then
        self:targetLost()
        return
    end

    -- it appears PLAYER_TARGET_CHANGED sometimes just fires
    --(for no reason?) during combat, without target change.
    -- happens shortly after entering combat.
    if guid == self.target_guid then
        return
    end

    self:targetAcquired(guid)
end
----------------------------------------
function Rotor:CheckInRange(spell_id)
    return not self.cur_mod or self.cur_mod:CheckInRange(spell_id)
end
----------------------------------------
function Rotor:spellcastFailed(spell, reason)
    local rec = self.spells[self.spell_id_cache[spell]]
    if rec then
        rec.sentAt = 0 -- fail send
        rec.doneAt = 0
    end

    if not self.cur_mod:CastFinished(spell, false, reason) then
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:queueCurrentModuleUpdate(force, reason)
    self:dprint('queue curr module update', reason, 'force =', force or false)
    if force then
        self:finalizeCurrentModule()
    end
    self.module_switch_time = GetTime() + 1
end
----------------------------------------
function Rotor:checkPet()
    self.pet_exists = UnitExists('pet')
    self.pet_alive = self.pet_exists and not UnitIsDead('pet')
end

----------------------------------------
--
-- Events
--
function Rotor:PLAYER_REGEN_ENABLED()
    self.in_combat = false
    self.cur_mod:CombatStateChanged()
    self:pluginsDo(function(plugin) plugin:CombatStateChanged() end)
    if self.target_guid then
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:PLAYER_REGEN_DISABLED()
    self.in_combat = true
    self.cur_mod:CombatStateChanged()
    if self.target_guid then
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:PLAYER_TARGET_CHANGED()
    self:checkTarget()
end
----------------------------------------
function Rotor:UNIT_FACTION()
    self:checkTarget()
end
----------------------------------------
function Rotor:PLAYER_ENTERING_WORLD()
    local _, t = GetInstanceInfo()
    if t == 'pvp' then
        self.player_in_pvp = true
    else
        self.player_in_pvp = false
    end

    self:checkTarget()
end
----------------------------------------
function Rotor:PARTY_MEMBERS_CHANGED()
    if GetNumRaidMembers() > 0 then
        self.player_in_raid = true
    else
        self.player_in_raid = false
    end

    if GetNumPartyMembers() > 0 then
        self.player_in_party = true
    else
        self.player_in_party = false
    end

    self:checkTarget()
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_SENT(caster, spell_name, rank, target)
    if caster == 'player' then
        local spell_id = self.spell_id_cache[spell_name]

        if spell_id ~= -1 then
            self.spells[spell_id].sentAt = GetTime()
        end

        self.cur_mod:CastSent(spell_name)
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_SUCCEEDED(caster, spell, rank)
    if caster == 'player' then
        local spell_id = self.spell_id_cache[spell]

        if spell_id ~= -1 then
            local rec = self.spells[spell_id]
            if rec then
                rec.doneAt = GetTime()
            end
        end

        if not self.cur_mod:CastFinished(spell, true) then
            self:forceUpdate()
        end
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_FAILED(caster, spell, rank)
    if caster == 'player' then
        self:spellcastFailed(spell, 'FAILED')
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_FAILED_QUIET(caster, spell, rank)
    if caster == 'player' then
        self:spellcastFailed(spell, 'FAILED_QUIET')
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_INTERRUPTED(caster, spell, rank)
    if caster == 'player' then
        self:spellcastFailed(spell, 'INTERRUPTED')
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_DELAYED(caster, spell, rank)
    if caster == 'player' then
        self:spellcastFailed(spell, 'DELAYED')
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_START(caster, spell, rank)
    if caster == 'player' then
        local _, _, _, _, startTime, endTime = UnitCastingInfo('player')
        self.spellcast_start_time = startTime / 1000.0
        self.spellcast_end_time = endTime / 1000.0
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_SPELLCAST_STOP(caster, spell, rank)
    if caster == 'player' then
        if self.spellcast_start_time < self.spellcast_end_time then
            self.spellcast_end_time = 0
            self:forceUpdate()
        end
    end
end
----------------------------------------
function Rotor:PLAYER_TALENT_UPDATE()
    self:queueCurrentModuleUpdate(false, 'PLAYER_TALENT_UPDATE')
end
----------------------------------------
function Rotor:ACTIVE_TALENT_GROUP_CHANGED()
    self:queueCurrentModuleUpdate(false, 'ACTIVE_TALENT_GROUP_CHANGED')
end
----------------------------------------
function Rotor:LEARNED_SPELL_IN_TAB()
    if (UnitLevel('player') ~= MAX_PLAYER_LEVEL)
        and not UnitIsDead('player') -- sometimes this event fired for unknown reason when player dies
    then
        self:queueCurrentModuleUpdate(true, 'LEARNED_SPELL_IN_TAB')
    end
end
----------------------------------------
function Rotor:UNIT_PET(unit)
    if unit == 'player' then
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:UNIT_INVENTORY_CHANGED(unitID)
    if unitID == 'player' then
        self.cur_mod:InventoryChanged()
    end
end
----------------------------------------
function Rotor:GLYPH_ADDED()
    self:queueCurrentModuleUpdate(false, 'GLYPH_ADDED')
end
----------------------------------------
function Rotor:GLYPH_REMOVED()
    self:queueCurrentModuleUpdate(false, 'GLYPH_REMOVED')
end
----------------------------------------
function Rotor:COMBAT_LOG_EVENT_UNFILTERED(...)
    if self.cur_mod:HandleUnfilteredCombatEvent(...) then
        self:forceUpdate()
    end
end
----------------------------------------
function Rotor:SPELL_UPDATE_USABLE()
    self:forceUpdate()
end
----------------------------------------
function Rotor:UNIT_AURA(unit)
    if (unit == 'player') or (unit == 'target') or (unit == 'pet') then
        if self:scanAuras(unit) then
            self:forceUpdate()
        end
    end
end
----------------------------------------
function Rotor:ACTIONBAR_UPDATE_COOLDOWN()
    self:forceUpdate()
end
----------------------------------------
function Rotor:SPELL_UPDATE_COOLDOWN()
    self:forceUpdate()
end
----------------------------------------
-- this event is registered in ADDON_LOADED and unregistered
-- once it fired. The reason for this is that on login, the
-- spellbook is not initialized, but after SKILL_LINES_CHANGED
-- fired.
function Rotor:SKILL_LINES_CHANGED()
    self:unregisterEvent('SKILL_LINES_CHANGED')

    -- find suitable spell for gcd_spell
    local reference_spells = {
        1978,   -- Serpent Sting(Hunter)
        47541,  -- Death Coil(Death Knight)
        45902,  -- Blood Strike (Death Knight)
        585,    -- Smite(Priest)
        20154,  -- Seal of Righteousness (Paladin)
        172,    -- Corruption(Warlock)
        42956,  -- Conjure Refreshment (Mage)
        772,    -- Rend(Warrior)
        94009,  -- Rend (Warrior)
        331,    -- Healing Wave(Shaman)
        1752,   -- Sinister Strike(Rogue)
        5176,   -- Wrath(Druid)
    }

    for _, spell in pairs(reference_spells) do
        if self:HasSpell(spell, true) then
            self.gcd_spell = spell
            break
        end
    end

    if self.gcd_spell == nil then
        self:print(L['Error: didn\'t find a reference spell for the global coldown, terminated.'])
        return
    end

    self:registerEvent('PLAYER_TALENT_UPDATE')
    self:registerEvent('ACTIVE_TALENT_GROUP_CHANGED')
    self:registerEvent('LEARNED_SPELL_IN_TAB')
    self:registerEvent('GLYPH_ADDED')
    self:registerEvent('GLYPH_REMOVED')

    self:queueCurrentModuleUpdate(false, 'SKILL_LINES_CHANGED')
end
----------------------------------------
function Rotor:ADDON_LOADED(addon)
    if addon ~= ADDON_NAME then
        return nil
    end

    self:unregisterEvent('ADDON_LOADED')

    self:initialize()
end
----------------------------------------
Rotor:registerEvent('ADDON_LOADED')
